package npr;

import processing.core.*;
import javax.media.opengl.*;
import com.sun.opengl.util.*;

/** Class for rendering to a texture directly which can be bigger than the screen size.
 * Uses framebuffer objects, but provides an automatic fallback to reading 
 * back from the normal framebuffer.
 */
public class RenderToTexture{

    /** The texture id of the color texture. */
    private int colorTexture;
    /** The texture id of the depth texture. */
    private int depthTexture;
    /**
     * The texture id of the texture used for storing the content of the previous framebuffer
     * in order to restore it after rendering to a texture when frambuffer objects are not supported.
     */
    private int previousFramebuffer;
    /** The id of the framebuffer object */
    private int frameBuffer;
    /** The width of the texture */
    private int width= 0;
    /** The height of the texture */
    private int height= 0;
    /** The wrap parameter for texture coordinate s. */
    private int wrap_s = GL.GL_CLAMP;    
    /** The wrap parameter for texture coordinate t. */
    private int wrap_t = GL.GL_CLAMP;
    /** The texture magnification function */
    private int mag_filter = GL.GL_LINEAR;
    /** The texture minifying function */
    private int min_filter = GL.GL_LINEAR;
    /**
     * A handle to the renderer to call it's methods. Always call renderer= (NPR)parent.g; 
     * before using this variable, as the renderer may change. 
     */
    private NPR renderer;
    /** The parent applet is set by the renderer calling setParent in its constructor. */
    private static PApplet parent;   
    /** A handle to the Renderer's gl object. Always call gl= renderer.gl; before using this variable */
    private GL gl;
    /** Specifies whether framebuffer objects are supported by the graphics card or not. */
    private boolean fboSupported= false;

    private int[] ids = {0};

    /**
     * Creates a RenderToTexture object and initializes it.
     * Checks if framebuffer objects are supported. If yes, creates a frame buffer 
     * object that is bound to a texture of a power of 2 size fitting or larger than the screen. 
     * If no, the size is set to the next smaller power of two to the screen size.
     */
    public RenderToTexture(){

	renderer= (NPR)parent.g;
	gl= renderer.gl;

	//Get all supported extensions
	String extensions = gl.glGetString(GL.GL_EXTENSIONS);     
	fboSupported= (extensions.indexOf("GL_EXT_framebuffer_object") != -1);
	
	if(fboSupported){
	    gl.glGenFramebuffersEXT(1, ids, 0);
	    frameBuffer = ids[0];
	    width= height= calculateNextBiggerSize();
	}
	else width= height= calculateNextSmallerSize();

	attachColorBuffer();
	attachDepthBuffer();
	
	if(fboSupported){
	    gl.glBindFramebufferEXT(GL.GL_FRAMEBUFFER_EXT, frameBuffer);
	    checkStatus();
	    gl.glBindFramebufferEXT(GL.GL_FRAMEBUFFER_EXT, 0);
	} 
	
	if(!fboSupported){
	    gl.glGenTextures(1, ids, 0);
	    previousFramebuffer = ids[0];
	    System.out.println("Framebuffer Objects not supported");
	}	
    }


    /**
     * Creates a RenderToTexture object and initializes it.
     * Checks if framebuffer objects are supported. If yes,  creates a frame buffer 
     * object that is bound to a texture of the specified size and format.
     * If no, the size is set to the next smaller power of two to the screen size.
     *
     * @param width should be power of 2 for performance reasons
     * @param height should be power of 2 for performance reasons  
    */
    public RenderToTexture(int width, int height){

	renderer= (NPR)parent.g;
	gl= renderer.gl;

	//Get all supported extensions
	String extensions = gl.glGetString(GL.GL_EXTENSIONS);     
	fboSupported= (extensions.indexOf("GL_EXT_framebuffer_object") != -1);
	
	if(!fboSupported && (width>parent.width || height>parent.height)) {
	    this.width= this.height= calculateNextSmallerSize();
	}
	else{
	    this.width =width;
	    this.height =height;
	}

	if(fboSupported){
	    gl.glGenFramebuffersEXT(1, ids, 0);
	    frameBuffer = ids[0];
	}

	attachColorBuffer();
	attachDepthBuffer();
	
	if(fboSupported){
	    gl.glBindFramebufferEXT(GL.GL_FRAMEBUFFER_EXT, frameBuffer);
	    checkStatus();
	    gl.glBindFramebufferEXT(GL.GL_FRAMEBUFFER_EXT, 0);
	} 
	
	if(!fboSupported){
	    gl.glGenTextures(1, ids, 0);
	    previousFramebuffer = ids[0];
	    System.out.println("Framebuffer Objects not supported");
	}
    }

    /**
     * Returns whether framebuffer objects are supported.
     *
     * @return true if framebuffer objects are supported, false if not.
     */
    public boolean fboSupported(){

	return fboSupported;
    }

    /**
     * Returns the width of the texture.
     *
     * @return the width of the texture.
     */
    public int getWidth(){

	return width;
    }

    /**
     * Returns the height of the texture.
     *
     * @return the height of the texture.
     */
    public int getHeight(){

	return height;
    }

    /**
     * Returns the wrap parameter for texture coordinate s.
     * 
     * @return either GL_CLAMP or GL_REPEAT.
     */   
    public int getWrapS(){

	return wrap_s;
    }

    /**
     * Returns the wrap parameter for texture coordinate t.
     * 
     * @return either GL_CLAMP or GL_REPEAT.
     */
    public int getWrapT(){

	return wrap_t;
    }

    /**
     * Returns the texture minifying function
     *
     * @return the texture minifying function (see OpenGL specification)
     */
    public int getMinFilter(){

	return min_filter;
    }

    /**
    * Returns the texture magnification function
     *
     * @return the texture magnification function (see OpenGL specification)
     */
    public int getMagFilter(){

	return mag_filter;
    }

    /**
     * Sets the wrap parameter for texture coordinate s.
     * 
     * @wrap_s either GL_CLAMP or GL_REPEAT.
     */  
    public void setWrapS(int wrap_s){

        this.wrap_s= wrap_s;
	bindColorTexture();
	gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, wrap_s);
	bindDepthTexture();
	gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, wrap_s);
	unbind();
    }

    /**
     * Sets the wrap parameter for texture coordinate t.
     * 
     * @wrap_t either GL_CLAMP or GL_REPEAT.
     */  
    public void setWrapT(int wrap_t){

	 this.wrap_t= wrap_t;
	 bindColorTexture();
	 gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, wrap_t);
	 bindDepthTexture();
	 gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, wrap_t);
	 unbind();
    }


    /**
    * Returns the texture minifying function
     *
     * @param min_filter the texture minifying function (see OpenGL specification)
     */
    public void setMinFilter(int min_filter){

	this.min_filter= min_filter;
	bindColorTexture();
	gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, min_filter);
	bindDepthTexture();
	gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, min_filter);
	unbind();
    }


    /**
     * Returns the texture magnification function
     *
     * @param mag_filter the texture magnification function (see OpenGL specification)
     */
    public void setMagFilter(int mag_filter){

	this.mag_filter= mag_filter;
	bindColorTexture();
	gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, mag_filter);
	bindDepthTexture();
	gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, mag_filter);
	unbind();
    }
 
    /**
     * Sets the parent applet. This is called by the renderer automatically.
     */
    protected static void setParent(PApplet applet){
	parent= applet;
    }


    /**
     * Calculates the next smaller power of two to the screen size.
     */
    protected int calculateNextSmallerSize(){
	int size= 1;
        while(size*2<=parent.width && size*2<=parent.height){
            size*=2;
        }
	return size;
    }

   /**
     * Calculates the next bigger power of two to the screen size.
     */
    protected int calculateNextBiggerSize(){
	int size= 1;
        while(size*2<parent.width && size*2<parent.height){
            size*=2;
        }
	size*=2;
	return size;
    }

    /**
     * Attaches a colour texture to the framebuffer object as a render target
     */
    protected void attachColorBuffer(){

	renderer= (NPR)parent.g;
	gl= renderer.gl;

	gl.glGenTextures( 1, ids, 0 );
	colorTexture = ids[0];

	gl.glPixelStorei(GL.GL_PACK_ALIGNMENT, 1);
	gl.glPixelStorei(GL.GL_UNPACK_ALIGNMENT, 1);
	
	if(fboSupported){

	    gl.glBindTexture(GL.GL_TEXTURE_2D, colorTexture);

	    gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, wrap_s);
	    gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, wrap_t);
	
	    gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, min_filter);	
	    gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, mag_filter);

	    gl.glTexImage2D(GL.GL_TEXTURE_2D, 0, GL.GL_RGBA16, width, height, 0, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, null);

	    gl.glBindTexture(GL.GL_TEXTURE_2D, 0);

	    gl.glBindFramebufferEXT(GL.GL_FRAMEBUFFER_EXT, frameBuffer);
	    
	    gl.glFramebufferTexture2DEXT(GL.GL_FRAMEBUFFER_EXT, GL.GL_COLOR_ATTACHMENT0_EXT, GL.GL_TEXTURE_2D, colorTexture, 0);

	    gl.glBindFramebufferEXT(GL.GL_FRAMEBUFFER_EXT, 0);	
	}
    }

    /**
     * Attaches a depth texture to the framebuffer object as a render target
     */
    protected void attachDepthBuffer(){

	renderer= (NPR)parent.g;
	gl= renderer.gl;

	gl.glGenTextures(1, ids, 0);
	depthTexture = ids[0];

	gl.glPixelStorei(GL.GL_PACK_ALIGNMENT, 1);
	gl.glPixelStorei(GL.GL_UNPACK_ALIGNMENT, 1);

	if(fboSupported){
	    gl.glBindTexture(GL.GL_TEXTURE_2D, depthTexture);

	    gl.glTexParameteri( GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, wrap_s );
	    gl.glTexParameteri( GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, wrap_t );
	    gl.glTexParameteri( GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, mag_filter );
	    gl.glTexParameteri( GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, min_filter );
	    
	    gl.glTexImage2D(GL.GL_TEXTURE_2D, 0, GL.GL_DEPTH_COMPONENT16, width, height, 0, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_SHORT, null);
	    
	    gl.glBindTexture(GL.GL_TEXTURE_2D, 0);
	    
	    gl.glBindFramebufferEXT(GL.GL_FRAMEBUFFER_EXT, frameBuffer);
       
	    //attach depth texture to framebuffer
	    gl.glFramebufferTexture2DEXT(GL.GL_FRAMEBUFFER_EXT, GL.GL_DEPTH_ATTACHMENT_EXT, GL.GL_TEXTURE_2D, depthTexture, 0);

	    gl.glBindFramebufferEXT(GL.GL_FRAMEBUFFER_EXT, 0);
	}
    }

    /**
     * Begin rendering to the texture.
     * All rendering operations are now bound to the FrameBuffer.
     */
    public void begin(){
	
	renderer= (NPR)parent.g;
	gl= renderer.gl;

	if(fboSupported){
	    gl.glBindFramebufferEXT(GL.GL_FRAMEBUFFER_EXT, frameBuffer);
	}
	else{
	    gl.glEnable(GL.GL_TEXTURE_2D);
	    gl.glBindTexture(GL.GL_TEXTURE_2D, previousFramebuffer);
	    gl.glCopyTexImage2D(GL.GL_TEXTURE_2D, 0, GL.GL_RGB, 0, 0, parent.width, parent.height, 0);
	    gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, GL.GL_CLAMP);
	    gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL.GL_CLAMP);
	    gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR);
	    gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR);
	    unbind();
	}

	gl.glEnable (GL.GL_DEPTH_TEST);
	gl.glClearColor(1f, 1f, 1f, 0f);
	gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
	gl.glDepthMask(true);
	gl.glDepthFunc(GL.GL_LEQUAL);

	gl.glPushAttrib(GL.GL_VIEWPORT_BIT);
	gl.glViewport(0,0,width, height);
    }

    /**
     * End rendring to the texture.
     * All rendering operations are now returned to the previous buffer (probably GL_BACK).
    */
    public void end(){
	
	renderer= (NPR)parent.g;
	gl= renderer.gl;
	
	if(fboSupported){
	    gl.glBindFramebufferEXT( GL.GL_FRAMEBUFFER_EXT, 0 );
	}
	else{

	    bindColorTexture();
	    gl.glCopyTexImage2D(GL.GL_TEXTURE_2D, 0, GL.GL_RGBA, 0, 0, width, height, 0);
	    
	    gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, wrap_s);
	    gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, wrap_t);
	    gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, mag_filter);
	    gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, min_filter);	  
	    
	    bindDepthTexture();
	    gl.glCopyTexImage2D(GL.GL_TEXTURE_2D, 0, GL.GL_DEPTH_COMPONENT16, 0, 0, width, height, 0);
	    
	    gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, wrap_s);
	    gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, wrap_t);
	    gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, mag_filter);
	    gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, min_filter);

	    gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);

	    gl.glBindTexture(GL.GL_TEXTURE_2D, previousFramebuffer);
	    gl.glColor3f(1.0f, 1.0f, 1.0f);
	    renderer.fullScreenQuad(1.0f);
	    unbind();	
	}
	gl.glPopAttrib();
    }


    /**
     * Applys the color texture to GL_TEXTURE_2D operations.
     */
    public void bindColorTexture(){

	renderer= (NPR)parent.g;
	gl= renderer.gl;

	gl.glEnable(GL.GL_TEXTURE_2D);
	gl.glBindTexture(GL.GL_TEXTURE_2D, colorTexture);
    }

    /**
     * Applys the depth texture to GL_TEXTURE_2D operations.
     */
    public void bindDepthTexture(){

	renderer= (NPR)parent.g;
	gl= renderer.gl;
	    
	gl.glEnable(GL.GL_TEXTURE_2D);
	gl.glBindTexture(GL.GL_TEXTURE_2D, depthTexture);
    }

    /**
     * Disables the texture.
     */
    public void unbind(){

	renderer= (NPR)parent.g;
	gl= renderer.gl;

	gl.glBindTexture(GL.GL_TEXTURE_2D, 0);
	gl.glDisable(GL.GL_TEXTURE_2D);
    }

    /**
     * Checks the status of the framebuffer object.
     */
    protected void checkStatus(){

	renderer= (NPR)parent.g;
	gl= renderer.gl;

	int status= gl.glCheckFramebufferStatusEXT(GL.GL_FRAMEBUFFER_EXT);
	switch(status) {
        case GL.GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT_EXT:
            System.out.println("FrameBufferObject incomplete, incomplete attachment");
            break;
        case GL.GL_FRAMEBUFFER_UNSUPPORTED_EXT:
            System.out.println("Unsupported FrameBufferObject format");
	    break;
        case GL.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_EXT:
            System.out.println("FrameBufferObject incomplete, missing attachment");
            break;
        case GL.GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_EXT:
            System.out.println("FrameBufferObject incomplete, attached images must have same dimensions");
	    break;
        case GL.GL_FRAMEBUFFER_INCOMPLETE_FORMATS_EXT:
	    System.out.println("FrameBufferObject incomplete, attached images must have same format");
          break;
        case GL.GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER_EXT:
            System.out.println("FrameBufferObject incomplete, missing draw buffer");
	    break;
        case GL.GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_EXT:
            System.out.println("FrameBufferObject incomplete, missing read buffer");
	    break;
	}
    }
} 
